package com.suncky.frame.http.file;

import android.content.Context;
import android.net.Uri;
import android.os.ParcelFileDescriptor;

import androidx.annotation.NonNull;

import com.suncky.frame.http.bean.Page;
import com.suncky.frame.http.bean.Result;
import com.suncky.frame.utils.GsonUtils;
import com.suncky.frame.utils.LogUtils;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.FileNameMap;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.BufferedSink;
import okio.ForwardingSink;
import okio.Okio;
import okio.Sink;

/**
 * 文件上传工具类
 * @author gl
 * @time 2021/4/16 15:20
 */
public class Uploader {
    private final Context context;
    private final MainThreadExecutor executor;
    private final String url;
    private final String formName;
    private final OkHttpClient okHttpClient;

    private Uploader(Builder builder) {
        this.context = builder.context;
        this.url = builder.url;
        this.formName = builder.formName;
        this.okHttpClient = builder.okHttpClient;
        this.executor = new MainThreadExecutor(builder.context);
    }

    public static class Builder {
        private final Context context;
        private String url;
        private String formName;
        private long timeout = 20;//超时时间
        private final List<Interceptor> interceptors;
        private OkHttpClient okHttpClient;

        public Builder(Context context) {
            this.context = context;
            interceptors = new ArrayList<>();
        }

        public Builder url(String url) {
            this.url=url;
            return this;
        }

        public Builder formName(String name) {
            this.formName = name;
            return this;
        }

        public Builder addInterceptor(Interceptor interceptor) {
            interceptors.add(interceptor);
            return this;
        }

        public Builder okHttpClient(OkHttpClient okHttpClient){
            this.okHttpClient = okHttpClient;
            return this;
        }

        /**
         * 设置超时时间(秒)
         * @param timeout 超时时间(秒)
         * @return
         */
        public Builder timeOut(long timeout) {
            this.timeout = timeout;
            return this;
        }

        public Uploader build() {
            if (okHttpClient == null) {
                okHttpClient = getDefaultOkHttpClient();
            } else {
                okHttpClient.interceptors().addAll(interceptors);
            }
            return new Uploader(this);
        }
        private OkHttpClient getDefaultOkHttpClient() {
            OkHttpClient.Builder builder = new OkHttpClient.Builder().connectTimeout(timeout, TimeUnit.SECONDS)
                    .readTimeout(timeout, TimeUnit.SECONDS).writeTimeout(timeout, TimeUnit.SECONDS);
            builder.interceptors().addAll(interceptors);
            return builder.build();
        }

    }

    /**
     * 获取文件上传请求的默认请求标签
     * @param uri
     * @return
     */
    @NonNull
    public Object defaultRequestTag(@NonNull Uri uri) {
        return uri.toString();
    }

    /**
     * 获取文件上传请求的默认请求标签
     * @param uris
     * @return
     */
    @NonNull
    public Object defaultRequestTag(@NonNull Uri[] uris) {
        if (uris.length == 0) {
            return new Object();
        }
        StringBuilder tagBuilder = new StringBuilder();
        for (Uri f :uris) {
            tagBuilder.append(f.toString());
        }
        return tagBuilder.toString();
    }

    /**
     * 获取文件上传请求的默认请求标签
     * @param file
     * @return
     */
    @NonNull
    public Object defaultRequestTag(@NonNull File file) {
        return file.getName();
    }

    /**
     * 获取文件上传请求的默认请求标签
     * @param files
     * @return
     */
    @NonNull
    public Object defaultRequestTag(@NonNull File[] files) {
        if (files.length == 0) {
            return new Object();
        }
        StringBuilder tagBuilder = new StringBuilder();
        for (File f :files) {
            tagBuilder.append(f.getName());
        }
        return tagBuilder.toString();
    }

    /**
     * 取消所有上传请求
     */
    public void cancel() {
        okHttpClient.dispatcher().cancelAll();
    }

    /**
     * 取消单文件上传请求
     * @param file 上传的文件
     */
    public void cancel(@NonNull File file) {
        cancel(defaultRequestTag(file));
    }

    /**
     * 取消多文件上传请求
     * @param files 上传的文件
     */
    public void cancel(@NonNull File[] files) {
        if (files.length == 0) {
            return;
        }
        cancel(defaultRequestTag(files));
    }

    /**
     * 取消上传请求
     * @param tag
     */
    public void cancel(Object tag) {
        for (Call call : okHttpClient.dispatcher().queuedCalls()) {
            if (Objects.equals(call.request().tag(), tag))
                call.cancel();
        }

        for (Call call : okHttpClient.dispatcher().runningCalls()) {
            if (Objects.equals(call.request().tag(), tag))
                call.cancel();
        }
    }

    /**
     * 上传单文件,使用默认标签标记请求
     * @param uri 要上传的文件uri
     * @param callBack
     */
    public <E,P extends Page,T extends Result<E,P>> void upload(@NonNull Uri uri, @NonNull UploadCallback<E,P,T> callBack) {
        upload(uri, defaultRequestTag(uri), callBack);
    }

    /**
     * 上传单文件,使用默认标签标记请求
     * @param uri 要上传的文件uri
     * @param callBack
     */
    public <E,P extends Page,T extends Result<E,P>> void upload(@NonNull Uri uri, @NonNull Object requestTag, @NonNull UploadCallback<E,P,T> callBack) {
        byte[] bytes;
        try {
            ParcelFileDescriptor fd= context.getContentResolver().openFileDescriptor(uri, "r");
            FileInputStream fis = new FileInputStream(fd.getFileDescriptor());
            bytes = new byte[fis.available()];
            if (fis.read(bytes) <= 0) {
                callBack.onFailure("the end of the file has been reached.");
                return;
            }
        } catch (IOException e) {
            e.printStackTrace();
            callBack.onFailure(e.getMessage());
            return;
        }
        UploadCallBackWrapper<E,P,T> callBackWrapper = new UploadCallBackWrapper<>(callBack);
        MultipartBody.Builder bodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM)
                .addFormDataPart(formName, uri.toString(),
                        new ProgressRequestBody<>(RequestBody.create(bytes,getMimeType(uri)), callBackWrapper));

        RequestBody requestBody = bodyBuilder.build();
        Request request = new Request.Builder().url(url).post(requestBody).tag(requestTag).build();
        callBack.onStart();
        Call call = okHttpClient.newCall(request);
        call.enqueue(callBackWrapper);
    }

    /**
     * 上传多文件,使用默认标签标记请求
     * @param uris 要上传的文件uri数组
     * @param callBack
     */
    public <E,P extends Page,T extends Result<E,P>> void upload(@NonNull Uri[] uris, @NonNull UploadCallback<E,P,T> callBack) {
        upload(uris, defaultRequestTag(uris), callBack);
    }

    /**
     * 上传多文件
     *
     * @param uris 要上传的文件uri数组
     * @param requestTag 标记上传请求的标签
     * @param callBack
     */
    public <E, P extends Page, T extends Result<E, P>> void upload(@NonNull Uri[] uris, @NonNull Object requestTag, @NonNull UploadCallback<E, P, T> callBack) {
        if (uris.length == 0) {
            return;
        }
        MultipartBody.Builder bodyBuilder = new MultipartBody.Builder();
        bodyBuilder.setType(MultipartBody.FORM);
        UploadCallBackWrapper<E, P, T> callBackWrapper = new UploadCallBackWrapper<>(callBack);
        for (Uri f : uris) {
            try {
                ParcelFileDescriptor fd= context.getContentResolver().openFileDescriptor(f, "r");
                FileInputStream fis = new FileInputStream(fd.getFileDescriptor());
                byte[] bytes = new byte[fis.available()];
                if (fis.read(bytes) <= 0) {
                    callBack.onFailure("the end of the file has been reached.");
                    return;
                }
                bodyBuilder.addFormDataPart(formName, f.toString(),
                        new ProgressRequestBody<>(RequestBody.create(bytes, getMimeType(f)), callBackWrapper));
            } catch (IOException e) {
                e.printStackTrace();
                callBack.onFailure(e.getMessage());
                return;
            }
        }
        RequestBody requestBody = bodyBuilder.build();
        Request request = new Request.Builder().url(url).post(requestBody).tag(requestTag).build();
        callBack.onStart();
        Call call = okHttpClient.newCall(request);
        call.enqueue(callBackWrapper);
    }


    /**
     * 上传单文件,使用默认标签标记请求
     * @param file 要上传的文件
     * @param callBack
     */
    public <E,P extends Page,T extends Result<E,P>> void upload(@NonNull File file, @NonNull UploadCallback<E,P,T> callBack) {
        upload(file, defaultRequestTag(file), callBack);
    }

    /**
     * 上传单文件
     * @param file 要上传的文件
     * @param requestTag 标记上传请求的标签
     * @param callBack
     */
    public <E,P extends Page,T extends Result<E,P>> void upload(@NonNull File file, @NonNull Object requestTag, @NonNull UploadCallback<E,P,T> callBack) {
        UploadCallBackWrapper<E,P,T> callBackWrapper = new UploadCallBackWrapper<>(callBack);
        MultipartBody.Builder bodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM)
                .addFormDataPart(formName, file.getName(),
                        new ProgressRequestBody<>(RequestBody.create(file, getMimeType(file.getName())), callBackWrapper));

        RequestBody requestBody = bodyBuilder.build();
        Request request = new Request.Builder().url(url).post(requestBody).tag(requestTag).build();
        callBack.onStart();
        Call call = okHttpClient.newCall(request);
        call.enqueue(callBackWrapper);
    }

    /**
     * 上传多文件,使用默认标签标记请求
     * @param files 要上传的文件数组
     * @param callBack
     */
    public <E,P extends Page,T extends Result<E,P>> void upload(@NonNull File[] files, @NonNull UploadCallback<E,P,T> callBack) {
        upload(files, defaultRequestTag(files), callBack);
    }

    /**
     * 上传多文件
     * @param files 要上传的文件数组
     * @param requestTag 标记上传请求的标签
     * @param callBack
     */
    public <E,P extends Page,T extends Result<E,P>> void upload(@NonNull File[] files,@NonNull Object requestTag, @NonNull UploadCallback<E,P,T> callBack) {
        if (files.length == 0) {
            return;
        }
        MultipartBody.Builder bodyBuilder = new MultipartBody.Builder();
        bodyBuilder.setType(MultipartBody.FORM);
        UploadCallBackWrapper<E,P,T> callBackWrapper = new UploadCallBackWrapper<>(callBack);
        for (File f : files) {
            bodyBuilder.addFormDataPart(formName, f.getName(),
                    new ProgressRequestBody<>(RequestBody.create(f, getMimeType(f.getName())), callBackWrapper));
        }
        RequestBody requestBody = bodyBuilder.build();
        Request request = new Request.Builder().url(url).post(requestBody).tag(requestTag).build();
        callBack.onStart();
        Call call = okHttpClient.newCall(request);
        call.enqueue(callBackWrapper);
    }

    /** 根据uri获取MIME类型 */
    private MediaType getMimeType(Uri uri) {
        if (uri == null) {
            return null;
        }
        String contentType = context.getContentResolver().getType(uri);
        if (contentType == null) {
            return MediaType.parse("application/octet-stream");
        }
        return MediaType.parse(contentType);
    }

    /** 根据文件名获取MIME类型 */
    private MediaType getMimeType(String fileName) {
        if (fileName == null) {
            return null;
        }
        FileNameMap fileNameMap = URLConnection.getFileNameMap();
        //解决文件名中含有#号异常的问题
        fileName = fileName.replace("#", "");
        String contentType = fileNameMap.getContentTypeFor(fileName);
        if (contentType == null) {
            return MediaType.parse("application/octet-stream");
        }
        return MediaType.parse(contentType);
    }

     class UploadCallBackWrapper<E,P extends Page,T extends Result<E,P>> implements Callback {

        private final UploadCallback<E,P,T> callBack;
        private long progress=0;
        private long total=0;

        public UploadCallBackWrapper(UploadCallback<E,P,T> callBack) {
            this.callBack = callBack;
        }

        @Override
        public void onFailure(@NotNull Call call, @NotNull IOException e) {
            resetProgress();
            if (call.isCanceled()) {
                executor.execute(()->callBack.onCancel(call.request().tag()));
            } else {
                executor.execute(()->callBack.onFailure(e.getMessage()));
            }
        }

        @SuppressWarnings("unchecked")
        @Override
        public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
            ResponseBody body = response.body();
            if (body == null) {
                resetProgress();
                executor.execute(() -> callBack.onFailure(response.message()));
                return;
            }
            T result;
            try {
                ParameterizedType pt = getParameterizedType(callBack.getClass());
                Class<T> tClass = (Class<T>) pt.getActualTypeArguments()[2];
                String content = body.string();
                result = GsonUtils.jsonToObject(tClass, content);
                body.close();
//                LogUtils.i("result--->"+content);
            } catch (Exception e) {
                resetProgress();
                executor.execute(() -> callBack.onFailure(e.getMessage()));
                return;
            }
            if (result.isSuccess()) {
                executor.execute(() -> callBack.onSuccess(result));
            } else {
                resetProgress();
                executor.execute(() -> callBack.onFailure(result.getMessage()));
            }
        }

        private ParameterizedType getParameterizedType(Class<?> cls) {
            if (cls == null) {
                return null;
            }
            Type type = cls.getGenericSuperclass();
            if (type instanceof ParameterizedType) {
                return (ParameterizedType) type;
            }
            //考虑连续继承多层的情况
            return getParameterizedType(cls.getSuperclass());
        }

        public long getTotal() {
            return total;
        }

        public long getProgress() {
            return progress;
        }

        public void setTotal(long total) {
            this.total = total;
        }

        public void setProgress(long progress) {
            this.progress = progress;
        }

        public void increaseTotal(long increase) {
            total += increase;
        }

        public void increaseProgress(long increase) {
            progress += increase;
        }

        public void resetProgress() {
            progress = 0;
            total = 0;
        }

        /**
         * 执行下载进度更新
         */
        public void executeProgress() {
            executor.execute(() -> callBack.onProgress(progress,total));
        }
    }

     static class ProgressRequestBody<E,P extends Page,T extends Result<E,P>> extends RequestBody {
        private static final String TAG = "ProgressRequestBody";
        private UploadCallBackWrapper<E,P,T> callBack;
        private final RequestBody requestBody;

        public ProgressRequestBody(RequestBody requestBody, UploadCallBackWrapper<E,P,T> callBack) {
            this.callBack = callBack;
            this.requestBody = requestBody;
            if (callBack != null) {
                callBack.increaseTotal(contentLength());
            }
        }

        @Nullable
        @Override
        public MediaType contentType() {
            return requestBody.contentType();
        }

        @Override
        public long contentLength()  {
            try {
                return requestBody.contentLength();
            } catch (IOException e) {
                e.printStackTrace();
                return -1;
            }
        }

        @Override
        public void writeTo(@NotNull BufferedSink bufferedSink) throws IOException {
            ProgressSink progressSink = new ProgressSink(bufferedSink);
            BufferedSink sink = Okio.buffer(progressSink);
            requestBody.writeTo(sink);
            sink.flush();
        }

        public void setCallBack(UploadCallBackWrapper<E,P,T> callBack) {
            this.callBack = callBack;
        }

        private final class ProgressSink extends ForwardingSink {

            public ProgressSink(@NotNull Sink delegate) {
                super(delegate);
            }

            @Override
            public void write(@NotNull Buffer source, long byteCount) throws IOException {
                super.write(source, byteCount);
//                LogUtils.d("ProgressSink.write "+byteCount+" bytes");
                if (callBack != null) {
                    callBack.increaseProgress(byteCount);
                    callBack.executeProgress();
//                LogUtils.i("current------>" + callBack.getProgress() + "  total------>" + callBack.getTotal());
                }
            }
        }

    }
}
